package cc.lishixi.tdd.parameterized;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.AggregateWith;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.converter.JavaTimeConversionPattern;
import org.junit.jupiter.params.provider.*;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
 * @author LiShixi 2021/1/1 6:36 下午
 * @email loyy77@163.com
 */
public class ParameterizedDemo {
    /**
     * 分别用 1 2 3 作为参数值，依次调用testWithValueSource方法，共执行三次测试方法。
     * @param argument
     */
    @ParameterizedTest
    @ValueSource(ints = { 1, 2, 3 })
    void testWithValueSource(int argument) {
        assertThat(argument).isGreaterThan(0).isLessThan(4);
    }

    /**
     * 为了测试被测类方法在接收各种“坏”输入值时方法的行为，我们要给参数化测试方法提供能够提供代表null和空值的参数值。
     * <code>@NullSource</code>：给参数化测试方法提供null作为参数值。这个注解不能用于提供基本类型的值。
     * <code>@EmptySource</code>：为参数化测试方法提供代表空（empty）的值作为参数值。支持以下类型：java.lang.String, java.util.List, java.util.Set, java.util.Map, 基本类型数组 (例如 int[], char[][], 等等), 对象数组 (例如String[], Integer[][]等等，但不支持上述类型的子类型。对于字符串，会提供空字符串；对于各种集合、Map和数组，提供不包含任何元素的空集合、空Map和空数组。
     * <code>@NullAndEmptySource</code>：包含了@NullSource 和 @EmptySource的组合注解。
     * 上述几个注解都只能应用在仅接收一个来自参数源的参数的参数化测试方法上。
     * @param text
     */
    @ParameterizedTest
    @NullSource
    @EmptySource
    @ValueSource(strings = { " ", "   ", "\t", "\n" })
    void nullEmptyAndBlankStrings(String text) {
        assertThat(text == null || text.trim().isEmpty()).isTrue();
    }

    /**
     * 上面的参数化方法分别使用参数null, "", " ", " ", "\t", "\n" 调用1次，一共6次。@NullSource提供第1个参数null，@EmptySource提供了第2个参数""，@ValueSource提供了其余的4个参数。参数化方法上注解出现顺序决定了参数的顺序。
     * 如果去掉@NullSource和@EmptySource，换成@NullAndEmptySource：
     * @param text
     */
    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = { " ", "   ", "\t", "\n" })
    void nullEmptyAndBlankStrings2(String text) {
        assertThat(text == null || text.trim().isEmpty()).isTrue();
    }

    /**
     * <code>@EnumSource</code>注解提供一个枚举类型的全部或部分枚举值来为参数化测试方法提供参数值。
     * @param unit
     */
    @ParameterizedTest
    @EnumSource
    void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
        System.out.println(unit);
    }

    /**
     * <code>@EnumSource</code>注解的值可以忽略掉。当没有给@EnumSource注解指定值时，会使用参数化测试的第一个参数的声明类型。
     * 上面的testWithEnumSource()方法必须给@EnumSource注解指定值，因为TemporalUnit不是枚举类型，而作为TemporalUnit接口的实现，ChronoUnit是枚举类型。
     * @param unit
     */
    @ParameterizedTest
    @EnumSource(ChronoUnit.class)
    void testWithEnumSource(TemporalUnit unit) {
        System.out.println(unit);
    }

    /**
     * 如果只想使用枚举类型中的部分枚举值，可以定义@EnumSource注解的names属性，包含那些要作为参数化方法的参数的枚举值：
     * @param unit
     */
    @ParameterizedTest
    @EnumSource(names = { "DAYS", "HOURS" })
    void testWithEnumSourceInclude(ChronoUnit unit) {
        assertThat(unit).isIn(ChronoUnit.DAYS, ChronoUnit.HOURS);
    }

    /**
     * 还可以通过定义@EnumSource注解的mode属性，微调枚举值的筛选方法。它有4个取值：
     * Mode.INCLUDE：默认选项。包含names属性中定义的枚举值。
     * Mode.EXCLUDE：排除names属性中定义的枚举值。
     * Mode.MATCH_ANY：当names是一组正则表达式时，返回匹配这些表达式之一的枚举值
     * Mode.MATCH_ALL：当names是一组正则表达式时，返回匹配全部这些表达式的枚举值
     * @param unit
     */
    @ParameterizedTest
    @EnumSource(mode = EnumSource.Mode.MATCH_ANY, names = "^.*DAYS$")
    void testWithEnumSourceRegex(ChronoUnit unit) {
        assertThat(unit.name()).endsWith("DAYS");
    }

    /**
     * @MethodSource注解使你可以调用测试类或外部类中的工厂方法来获得参数化测试方法的参数值。
     * 如果工厂方法来自测试类，除非采用了PER_CLASS生命周期，否则这个方法必须是静态的；如果工厂方法来自外部类，它必须是静态的。这些工厂方法必须没有任何参数。
     * 每个工厂方法必须能够生成一个由参数集组成的流，每个参数集中各个参数值按顺序提供给参数化方法的各个参数。这里所说的“流”是指所有可以被JUnit转换为Stream类型的任何类型，如Stream, DoubleStream, LongStream, IntStream, Collection, Iterator, Iterable，对象数组，原始类型数组，等等。流中的元素也可以作为Arguments类的实例、对象数组、单个值（如果参数化测试方法只接受单个参数）等提供给参数化测试方法。
     * @param argument
     */
    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithExplicitLocalMethodSource(String argument) {
        assertThat(argument).isIn("apple", "banana");
    }

    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana");
    }

    /**
     * 如果你没有在@MethodSource注解中指定工厂方法的名字，JUnit Jupiter将在测试类中寻找和参数化测试方法同名的方法作为工厂方法。下面是示例：
     * @return
     */
    static Stream<String> testWithDefaultLocalMethodSource() {
        return Stream.of("apple", "banana");
    }
    @ParameterizedTest
    @MethodSource("range")
    void testWithRangeMethodSource(int argument) {
        assertThat(argument).isLessThan(20).isGreaterThan(9);
    }

    static IntStream range() {
        return IntStream.range(0, 20).skip(10);
    }

    /**
     * 如果参数化测试方法声明多个参数，工厂方法必须返回以Arguments类型的对象为元素的流（流、集合、数组等等）。下面是代码示例：
     * @param str
     * @param num
     * @param list
     */
    @ParameterizedTest
    @MethodSource("stringIntAndListProvider")
    void testWithMultiArgMethodSource(String str, int num, List<String> list) {
        assertThat(str).hasSize(5);
        assertThat(num).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2);
        assertThat(list).hasSize(2);
    }

    static Stream<Arguments> stringIntAndListProvider() {
        return Stream.of(
                Arguments.arguments("apple", 1, Arrays.asList("a", "b")),
                Arguments.arguments("lemon", 2, Arrays.asList("x", "y"))
        );
    }
    /**
     *下面是使用外部类的静态工厂方法的例子。首先定义一个类StringsProviders
     *
     */
    @ParameterizedTest
    @MethodSource("cc.lishixi.tdd.parameterized.StringsProviders#tinyStrings")
    void testWithExternalMethodSource(String tinyString) {
        assertThat(tinyString).isIn(".", "oo", "OOO");
    }

    /**
     * @CsvSource注解允许你使用CSV形式给参数化方法提供参数：
     * @param fruit
     * @param rank
     */

    @ParameterizedTest
    @CsvSource({
            "apple,         1",
            "banana,        2",
            "'lemon, lime', 0xF1"
    })
    void testWithCsvSource(String fruit, int rank) {
        assertThat(fruit).isIn("apple", "banana", "lemon, lime");
        assertThat(rank).isNotEqualTo(0);
    }

    /**
     * @CsvFileSource注解让你可以用类路径上的CSV文件来为参数化测试提供参数。
     * 我们在类路径根目录下提供一个CSV文件two-column.csv，内容如下：
     * @param country
     * @param reference
     */

    @ParameterizedTest
    @CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1, encoding = "UTF-8")
    void testWithCsvFileSource(String country, int reference) {
        assertThat(country).isIn("Sweden", "Poland", "United States of America");
        assertThat(reference).isPositive();
    }

    /**
     * @ArgumentsSource注解指定一个ArgumentsProvider的实现类，通过该类的provideArguments方法来为参数化测试方法提供参数。这个ArgumentsProvider必须是顶层类或静态嵌套类。
     * @param argument
     */
    @ParameterizedTest
    @ArgumentsSource(MyArgumentsProvider.class)
    void testWithArgumentsSource(String argument) {
        assertThat(argument).isIn("apple", "banana");
    }

    /**
     * testWithArgumentsSource 有关
     */

    static class MyArgumentsProvider implements ArgumentsProvider {

        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
            return Stream.of("apple", "banana").map(Arguments::of);
        }
    }

    /**
     * 为了支持类似@CsvSource这样的数据源，JUnit Jupiter提供了一些内建的隐式类型转换器。具体转换过程由参数化方法中每个参数的声明类型决定。
     * 例如，如果一个参数化测试方法声明了一个TimeUnit类型的参数，但是参数源提供的参数的实际类型是String，那么这个字符串值将被自动转换成TimeUnit相应的一个枚举值。
     * @param argument
     */
    @ParameterizedTest
    @ValueSource(strings = "SECONDS")
    void testWithImplicitArgumentConversion(ChronoUnit argument) {
        assertNotNull(argument.name());
    }

    /**
     * 工厂方法和工厂构造函数转换
     * 如果参数化测试方法的参数类型声明了一个且只有一个工厂方法或工厂构造函数，那么JUnit Jupiter也会自动地将参数源提供的String值转换成目标参数类型的实例。
     * 工厂方法：在目标参数类型上声明的一个非私有的、静态的方法，接受一个字符串类型的参数并返回目标类型的实例。工厂方法名字是任意的，不需要遵守任何规则。
     * 工厂构造函数：在目标参数类型上声明的一个非私有的构造函数，只接受一个字符串类型的参数。注意：目标;类型必须是顶层类或静态嵌套类。
     * 如果同时存在多个工厂方法，则这些工厂方法都会被忽略。如果工厂方法和工厂构造函数共存，那么将使用工厂方法进行转换。
     * 例如在下面的参数化测试方法代码示例中，将会通过调用Book.fromTitle()静态工厂方法并传递字符串42 Cats作为它的参数来创建Book类的实例，传递给参数化测试方法testWithImplicitFallbackArgumentConversion()的形式参数book。
     * @param book
     */
    @ParameterizedTest
    @ValueSource(strings = "42 Cats")
    void testWithImplicitFallbackArgumentConversion(Book book) {
        assertEquals("42 Cats", book.getTitle());
    }
    public static class Book {

        private final String title;

        private Book(String title) {
            this.title = title;
        }

        public static Book fromTitle(String title) {
            return new Book(title);
        }


        public String getTitle() {
            return this.title;
        }
    }

    /**
     * 我们还可以对参数化测试声明参数显式转换。对于参数化测试中的一个参数：
     * 定义一个ArgumentConverter，用来把其他类型的值转换成目标参数类型的值；
     * 在目标参数上添加注解@ConvertWith，引用上面的ArgumentConverter。
     * ArgumentConverter必须是顶层类或静态嵌套类。
     * 下面给出一个例子。首先定义一个ArgumentConverter接口的实现类，能够将其他类型的对象转换成字符串（如果是枚举类型的就转换成枚举值的名字，其他的就是对象的toString()结果）：
     * @param argument
     */
    @ParameterizedTest
    @EnumSource(ChronoUnit.class)
    void testWithExplicitArgumentConversion(
            @ConvertWith(ToStringArgumentConverter.class) String argument) {
        System.out.println(argument);
    }

    /**
     * junit-jupiter-params工件中包含一个参数转换器JavaTimeArgumentConverter，能够将字符串形式的日期/时间转换成LocalData/LocalTime/LocalDateTime等。可以通过组合注解JavaTimeConversionPattern使用它：
     * @param argument
     */
    @ParameterizedTest
    @ValueSource(strings = { "01.01.2017", "31.12.2017" })
    void testWithExplicitJavaTimeConverter(
            @JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
        assertThat(argument.getYear()).isEqualTo(2017);
    }


    /**
     * 参数聚合
     * 有时候，参数源提供的参数非常多，会导致参数化测试方法的方法签名非常长。
     * 这时候你可以在参数化测试方法中用一个参数聚合器ArgumentsAccessor取代一整批参数。这种方式下同样支持参数的隐式转换。
     * @param arguments
     */

    @ParameterizedTest
    @CsvSource({
            "Jane, Doe, F, 1990-05-20",
            "John, Doe, M, 1990-10-22"
    })
    void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
        Person person = new Person(arguments.getString(0),
                arguments.getString(1),
                arguments.get(2, Gender.class),
                arguments.get(3, LocalDate.class));

        if (person.getFirstName().equals("Jane")) {
            assertThat(person.getGender()).isEqualTo(Gender.F);
        }
        else {
            assertThat(person.getGender()).isEqualTo(Gender.M);
        }
        assertThat(person.getLastName()).isEqualTo("Doe");
        assertThat(person.getDateOfBirth().getYear()).isEqualTo(1990);
    }

    /**
     * 这样你就能够在参数化测试方法中使用PersonAggregator了：
     * @param person
     */
    @ParameterizedTest
    @CsvSource({
            "Jane, Doe, F, 1990-05-20",
            "John, Doe, M, 1990-10-22"
    })
    void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
        if (person.getFirstName().equals("Jane")) {
            assertThat(person.getGender()).isEqualTo(Gender.F);
        }
        else {
            assertThat(person.getGender()).isEqualTo(Gender.M);
        }
        assertThat(person.getLastName()).isEqualTo("Doe");
        assertThat(person.getDateOfBirth().getYear()).isEqualTo(1990);
    }

    /**
     * 如果你发现在代码中多次使用@AggregateWith(PersonAggregator.class)，更好的方式是定义一个组合注解@CsvToPerson,使用PersonAggregator作为它的元注解：
     * @param person
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    @AggregateWith(PersonAggregator.class)
    public @interface CsvToPerson {
    }

    /**
     * 然后在参数化测试方法中定义Person类型的参数，注解为@CsvToPerson：
     * @param person
     */
    @ParameterizedTest
    @CsvSource({
            "Jane, Doe, F, 1990-05-20",
            "John, Doe, M, 1990-10-22"
    })
    void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
        if (person.getFirstName().equals("Jane")) {
            assertThat(person.getGender()).isEqualTo(Gender.F);
        }
        else {
            assertThat(person.getGender()).isEqualTo(Gender.M);
        }
        assertThat(person.getLastName()).isEqualTo("Doe");
        assertThat(person.getDateOfBirth().getYear()).isEqualTo(1990);
    }

    /**
     *  定制显示名
     * 可以类似下面的示例那样定义参数化测试方法的显示名：
     * <table>
     *     <th><td>占位符</td><td>占位符</td></th>
     *     <tr><td>${displayName}</td><td>方法的显示名</td></tr>
     *     <tr><td>${index}</td><td>当前调用次数（从1开始）</td></tr>
     *     <tr><td>${arguments}</td><td>逗号分割的完整参数列表</td></tr>
     *     <tr><td>${argumentsWithNames}</td><td>逗号分割的带参数名的完整参数列表</td></tr>
     *     <tr><td>${0},{1},....</td><td>具体的参数</td></tr>
     * </table>
     * @param fruit
     * @param rank
     */
    @DisplayName("Display name of container")
    @ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
    @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
    void testWithCustomDisplayNames(String fruit, int rank) {
    }
}
